<!DOCTYPE html>
<html>
<!--
Copyright 2011 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<head>
<title>Model Tests</title>
<script src="third_party/closure/closure/goog/base.js"></script>
<script src="platform/compat.js"></script>
<script src="side_table.js"></script>
<script src="path.js"></script>
<script src="model.js"></script>
<script src="test_common.js"></script>

<script>
goog.require('goog.testing.jsunit');
</script>
</head>
<body>
<script>

// Closure's assertArrayEquals does not handle proxies
function assertArrayEquals() {
  var msg = '', a, b;
  if (arguments.length == 3) {
    msg = arguments[0];
    a = arguments[1];
    b = arguments[2];
  } else {
    a = arguments[0];
    b = arguments[1];
  }
  assertEquals('different type', typeof a, typeof b);
  assertEquals('different length', a.length, b.length);
  for (var i = 0; i < a.length; i++) {
    assertEquals('index ' + i, a[i], b[i]);
  }
}

function testObserveProperty() {
  var data = { id: 1 };
  var model = data;
  var eventCount = 0;

  Model.observe(model, 'id', function(c) {
    ++eventCount;
  });

  // Event should fire.
  model.id = 2;
  Model.dirtyCheck();
  assertEquals(1, eventCount);
  assertEquals(model.id, data.id);

  data.id = 3;
  Model.dirtyCheck();

  if (Model.observableObjects_)
    assertEquals(1, eventCount); // Change to raw object wasn't detected.
  else
    assertEquals(2, eventCount); // Forced dirty checking caught the change.
  assertEquals(model.id, data.id);

}

function testObjectPropertySet() {
  var data = {};
  var model = data;

  var eventCount = 0;
  var expect = {
    propertyName: 'id'
  };

  var callback = function(c) {
    ++eventCount;
    assertEquals(expect.propertyName, c.propertyName);
    assertEquals(expect.mutation, c.mutation);
  };

  Model.observePropertySet(model, callback);

  expect.mutation = 'add';
  model.id = 0;
  Model.dirtyCheck();
  assertEquals(1, eventCount);

  expect.mutation = 'delete';
  delete model.id;
  Model.dirtyCheck();
  assertEquals(2, eventCount);

  // Stop observing -- shouldn't see an event
  Model.stopObservingPropertySet(model, callback);
  model.id = 101;
  Model.dirtyCheck();
  assertEquals(2, eventCount);

  // Re-observe -- should see an new event again.
  Model.observePropertySet(model, callback);
  expect.mutation = 'delete';
  delete model.id;
  Model.dirtyCheck();
  assertEquals(3, eventCount);
}

function testObjectDeleteAddDelete() {
  var model = { id: 1 };
  var eventCount = 0;
  Model.observePropertySet(model, function() {
    eventCount++;
  })

  // If mutation occurs in seperate "runs", two events fire.
  delete model.id;
  Model.dirtyCheck();
  model.id = 1;
  Model.dirtyCheck();
  assertEquals(2, eventCount);

  // If mutation occurs in the same "run", no events fire (nothing changed).
  delete model.id;
  model.id = 1;
  Model.dirtyCheck();
  assertEquals(2, eventCount);
}

function testGetPath() {
  var m = {a: {b: {c: 'xyz'}}};

  assertEquals(undefined, Model.getValueAtPath(m, 'a.b.c.d'));
  assertEquals('xyz', Model.getValueAtPath(m, 'a.b.c'));
  assertEquals(m.a.b, Model.getValueAtPath(m, 'a.b'));
  assertEquals(m.a, Model.getValueAtPath(m, 'a'));
}

function testMutationWithinObserver() {
  var m = {};
  var count = 0;

  Model.observe(m, 'foo', function() {
    count++;
    m.foo = 'bat';
  });

  m.foo = 'bar';
  Model.dirtyCheck();
  assertEquals(2, count);
}

function testPathValueBreadthFirstNotification() {
  var m = {};

  var notificationSequence = '';
  function createCallback() {
    return function(obj) {
      notificationSequence += obj.val;
    }
  };

  Model.observe(m, 'data.a.c', createCallback());
  Model.observe(m, 'data.a.d', createCallback());
  Model.observe(m, 'data.b.e', createCallback());
  Model.observe(m, 'data.b.f', createCallback());
  Model.observe(m, 'data.b', createCallback());
  Model.observe(m, 'data.a', createCallback());
  Model.observe(m, 'data', createCallback())
  Model.observePropertySet(m, function(c) {
    notificationSequence += 'add,';
  });

  m.data = {
    val: '0',
    a: {
      val: '1',
      c: {
        val: '2'
      },
      d: {
        val: '2'
      }
    },
    b: {
      val: '1',
      e: {
        val: '2'
      },
      f: {
        val: '2'
      }
    }
  }

  Model.dirtyCheck();
  assertEquals('add,0112222', notificationSequence);
}

function testPathValueDescendantsCanBeRemovedDuringNotification() {
  var m = [{a: 2}, {a: 1}];

  var eventCount = 0;
  var observer = function() {
    eventCount++;
  };
  var pv = Model.observe(m, '[1].a', observer);

  // First test that the splice actually propagates down.
  m.unshift({a: 3});
  Model.dirtyCheck();
  assertEquals(1, eventCount);

  // Now remove the observation above while a higher-level is receiving the
  // notification.
  Model.observePropertySet(m, function(c) {
    Model.stopObserving(m, '[1].a', observer);
  });

  // The observation above should not have fired.
  m.unshift({a: 4});
  Model.dirtyCheck();
  assertEquals(1, eventCount);
}

function testPathValueDescendantsCanBeResetDuringNotification() {
  var m = [{a: 2}, {a: 1}];

  var eventCount = 0;
  var observer = function() {
    eventCount++;
  };
  var pv = Model.observe(m, '[1].a', observer);

  // First test that the splice actually propagates down.
  m.unshift({a: 3});
  Model.dirtyCheck();
  assertEquals(1, eventCount);

  // Now reset the observation above while a higher-level is receiving the
  // notification. The 're-observe' should reset the lastObservedValue,
  // and prevent the notification from firing.
  Model.observePropertySet(m, function(c) {
    Model.stopObserving(m, '[1].a', observer);
    Model.observe(m, '[1].a', observer);
  });

  // The observation above should not have fired.
  m.unshift({a: 4});
  Model.dirtyCheck();
  assertEquals(1, eventCount);
}


function testPathObservation() {
  var m = {
    a: {
      b: {
        c: 'hello, world'
      }
    }
  };

  var eventCount = 0;
  var expect = {
    oldModel: undefined,
    model: 'hello, world'
  }

  var observer = function(model, oldModel) {
    eventCount++;
    assertEquals(expect.oldModel, oldModel);
    assertEquals(expect.model, model);
  };

  Model.observe(m, 'a.b.c', observer);

  assertEquals(expect.model, Model.getValueAtPath(m, 'a.b.c'));

  expect.oldModel = expect.model;
  expect.model = 'hello, mom';
  m.a.b.c = 'hello, mom';
  Model.dirtyCheck();
  assertEquals(1, eventCount);
  assertEquals(expect.model, Model.getValueAtPath(m, 'a.b.c'));

  expect.oldModel = expect.model;
  expect.model = 'hello, dad';
  m.a.b = {
    c: 'hello, dad'
  };
  Model.dirtyCheck();
  assertEquals(2, eventCount);
  assertEquals(expect.model, Model.getValueAtPath(m, 'a.b.c'));

  expect.oldModel = expect.model;
  expect.model = 'hello, you';
  m.a = {
    b: {
      c: 'hello, you'
    }
  };
  Model.dirtyCheck();
  assertEquals(3, eventCount);
  assertEquals(expect.model, Model.getValueAtPath(m, 'a.b.c'));

  // a.b.c is no longer able to evaluate
  expect.oldModel = expect.model;
  expect.model = undefined;
  m.a.b = 1;
  Model.dirtyCheck();
  assertEquals(4, eventCount);
  assertEquals(expect.model, Model.getValueAtPath(m, 'a.b.c'));

  // Stop observing
  Model.stopObserving(m, 'a.b.c', observer);
  expect.oldModel = expect.model;
  expect.model = 'hello, back again -- but not observing';
  m.a.b = {c: 'hello, back again -- but not observing'};
  Model.dirtyCheck();
  assertEquals(4, eventCount);

  // Resume observing
  Model.observe(m, 'a.b.c', observer);
  expect.oldModel = expect.model;
  expect.model = 'hello. Back for reals';
  m.a.b.c = 'hello. Back for reals';
  Model.dirtyCheck();
  assertEquals(5, eventCount);

  // Try to stop observing at different path. Scopes are different,
  // so this should have no effect.
  Model.stopObserving(m.a, 'b.c', observer)
  expect.oldModel = expect.model;
  expect.model = 'hello. scopes are different';
  m.a.b.c = 'hello. scopes are different';
  Model.dirtyCheck();
  assertEquals(6, eventCount);
}

function testMultipleObservationsAreCollapsed() {
  var m = {id: 1};
  var count = 0;
  var observer = function() {
    count++;
  }

  var pv1 = Model.observe(m, 'id', observer);
  var pv2 = Model.observe(m, 'id', observer);

  assertEquals(pv1, pv2);
  m.id = 2;
  Model.dirtyCheck();
  assertEquals(1, count);
}

function testExceptionDoesntStopNotification() {
  var m = { id: 1 };
  var count = 0;;
  Model.observe(m, 'id', function() {
    count++;
    throw 'Bad';
  });
  Model.observe(m, 'id', function() {
    count++;
    throw 'News';
  });
  Model.observePropertySet(m, function() {
    count++;
    throw 'Bears';
  });
  Model.observePropertySet(m, function() {
    count++;
    throw 'Rule!';
  });

  m.id = 2;
  m.id2 = 2;
  assertThrows(function() {
    Model.dirtyCheck();
  });
  assertEquals(4, count);
}

function testSetSame() {
  var m = {id: 1};
  var fired = false;
  Model.observePropertySet(m, function(c) {
    fired = true;
  });
  m.id = 1;
  Model.dirtyCheck();
  assertFalse('Setting an own property to same value should not notify', fired);
}

function testSetToSameAsPrototype() {
  var m = createObject({
    __proto__: {
      id: 1
    }
  });
  var fired = false;
  Model.observePropertySet(m, function(c) {
    fired = true;
  });
  m.id = 1;
  Model.dirtyCheck();
  assertFalse('Setting a property to same value should not notify', fired);
}

function testSetReadOnly() {
  var obj = {};
  Object.defineProperty(obj, 'x', {
    configurable: true,
    writable: false,
    value: 1
  });
  var m = obj;

  var fired = false;
  Model.observePropertySet(m, function(c) {
    fired = true;
  });
  m.x = 2;
  Model.dirtyCheck();
  assertFalse('Setting a read only property should not notify', fired);
}

function testSetUndefined() {
  var m = {};

  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals('x', c.propertyName);
    assertEquals('add', c.mutation);
    assertUndefined(c.value);
    assertUndefined(c.oldValue);
    count++;
  });
  m.x = undefined;
  Model.dirtyCheck();
  assertEquals(1, count);
}

function testSetShadows() {
  var m = createObject({
    __proto__: {
      x: 1
    }
  });

  var count = 0;
  Model.observe(m, 'x', function(newValue, oldValue) {
    assertEquals(2, newValue);
    assertEquals(1, oldValue);
    count++;
  });
  m.x = 2;
  Model.dirtyCheck();
  assertEquals(1, count);
}

function testDeleteWithSameValueOnPrototype() {
  var m = createObject({
    __proto__: {
      x: 1,
    },
    x: 1
  });

  var fired = false;
  Model.observePropertySet(m, function(c) {
    fired = true;
  });
  assertTrue(delete m.x);
  Model.dirtyCheck();
  assertFalse('Deleting a property exposes the same value in this case', fired);
}

function testDeleteWithDifferentValueOnPrototype() {
  var m = createObject({
    __proto__: {
      x: 1,
    },
    x: 2
  });

  var count = 0;
  Model.observe(m, 'x', function(newValue, oldValue) {
    count++;
    assertEquals(1, newValue);
    assertEquals(2, oldValue);
  });
  assertTrue(delete m.x);
  Model.dirtyCheck();
  assertEquals(1, count);
}

function testDeleteOfNonConfigurable() {
  var obj = {};
  Object.defineProperty(obj, 'x', {
    configurable: false,
    value: 1
  });
  var m = obj;

  var count = 0;
  Model.observePropertySet(m, function(c) {
    count++;
  });
  assertFalse(delete m.x);
  Model.dirtyCheck();
  assertEquals(0, count);
}

function testModelWithFunctions() {
  var object = {
    innerObject: {},
    field: 42,
    method1: function() {
      return this.field;
    },
    method2: function() {
      return 42;
    },
    getInnerObject: function() {
      return this.innerObject;
    },

    modifyField: function() {
      // "this" is not wrapped
      assertEquals(object, this);
      this.field++;
    }
  };
  var m = object;

  assertEquals(42, m.field);
  assertEquals(42, m.method1());
  assertEquals(42, m.method2());

  var g = m.method1.bind(m);
  assertEquals('Test that bind works correctly', 42, g());

  var f = m.method2;
  assertEquals(42, f());

  var count = 0;
  Model.observePropertySet(f, function(c) {
    assertEquals('test', c.propertyName);
    count++;
  });
  f.test = 1;
  Model.dirtyCheck();
  assertEquals(1, count);

  Model.observePropertySet(m, function(c) {
    assertEquals('field', c.propertyName);
    count++;
  });
  m.modifyField();
  Model.dirtyCheck();
  assertEquals(1, count);

  var innerObject = m.getInnerObject();
  Model.observePropertySet(innerObject, function(c) {
    count++;
  });

  assertEquals('Ensure that we return wrapped objects',
               m.innerObject, innerObject);

  innerObject.x = 'y';
  Model.dirtyCheck();
  assertEquals(2, count);
}

function testModelFunctionConstruct() {
  if (!Model.observableObjects_)
    return;

  var m = function(x, y, z) {
    assertEquals('x', x);
    assertEquals('y', y);
    assertEquals('z', z);

    this.change = function() {
      this.test++;
    };

    var self = this;
    this.change2 = function() {
      self.test++;
    };

    this.change3 = function(obj) {
      this.test += Number(obj !== obj);
    }
  };

  var object = new m('x', 'y', 'z');

  var count = 0;
  Model.observePropertySet(object, function(c) {
    assertEquals('test', c.propertyName);
    count++;
  });
  object.test = 1;
  Model.dirtyCheck();
  assertEquals(1, count);

  // "this" is not wrapped
  object.change();
  Model.dirtyCheck();
  assertEquals(2, object.test);
  assertEquals(1, count);

  // Internal change not observable.
  object.change2();
  Model.dirtyCheck();
  assertEquals(3, object.test);
  assertEquals(1, count);

  // Arguments should be unwrapped.
  object.change3({});
  Model.dirtyCheck();
  assertEquals(4, object.test);
  assertEquals(1, count);
}

function testModelFunctionConstructReturnObject() {
  var m = function(x, y, z) {
    assertEquals('x', x);
    assertEquals('y', y);
    assertEquals('z', z);

    return {};
  };

  var object = new m('x', 'y', 'z');

  var count = 0;
  Model.observePropertySet(object, function(c) {
    assertEquals('test', c.propertyName);
    count++;
  });
  object.test = 1;
  Model.dirtyCheck();
  assertEquals(1, count);
}

function testArrayModel() {
  var zero = {zero: 0};
  var one = {one: 1};
  var m = [zero, one];

  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals('splice', c.mutation);
    count++;
  });

  var two = {two: 2};
  var three = {three: 3};
  m[0] = two;
  Model.dirtyCheck();

  assertArrayEquals([two, one], m);
  assertEquals(1, count);

  m[1] = three;
  Model.dirtyCheck();

  assertArrayEquals([two, three], m);
  assertEquals(2, count);
}

function testArrayModelSplice() {
  var zero = {zero: 'zero'};
  var one = {one: 'one'};
  var two = {two: 'two'};
  var three = {three: 'three'};
  var zeroModel = zero;
  var oneModel = one;
  var twoModel = two;
  var threeModel = three;

  var arr = [zero, one];
  var m = arr;

  var expected = {};
  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals(expected.mutation, c.mutation);
    assertEquals(expected.index, c.index);
    assertArrayEquals(expected.removed, c.removed);
    assertArrayEquals(expected.added, c.added);
    count++;
  });

  expected.mutation = 'splice';
  expected.index = 1;
  expected.removed = [oneModel];
  expected.added = [twoModel, threeModel];

  m.splice(1, 1, two, three);

  Model.dirtyCheck();
  assertArrayEquals([zero, two, three], arr);
  assertArrayEquals([zeroModel, twoModel, threeModel], m);
  assertEquals(1, count);

  expected.mutation = 'splice';
  expected.index = 0;
  expected.removed = [zeroModel];
  expected.added = [];

  m.splice(0, 1);

  Model.dirtyCheck();
  assertArrayEquals([two, three], arr);
  assertArrayEquals([twoModel, threeModel], m);
  assertEquals(2, count);

  m.splice();
  Model.dirtyCheck();
  assertEquals(2, count);

  m.splice(0, 0);
  Model.dirtyCheck();
  assertEquals(2, count);

  m.splice(0, -1);
  Model.dirtyCheck();
  assertEquals(2, count);

  expected.mutation = 'splice';
  expected.index = 1;
  expected.removed = [];
  expected.added = [1.5];

  m.splice(-1, 0, 1.5);
  Model.dirtyCheck();
  assertArrayEquals([two, 1.5, three], arr);
  assertArrayEquals([twoModel, 1.5, threeModel], m);

  expected.mutation = 'splice';
  expected.index = 3;
  expected.removed = [];
  expected.added = [zeroModel];

  m.splice(3, 0, zeroModel);
  Model.dirtyCheck();
  assertArrayEquals([two, 1.5, three, zero], arr);
  assertArrayEquals([twoModel, 1.5, threeModel, zeroModel], m);

  expected.mutation = 'splice';
  expected.index = 0;
  expected.removed = [twoModel, 1.5, threeModel, zeroModel];
  expected.added = [];

  m.splice(0);
  Model.dirtyCheck();
  assertEquals(5, count);
  assertArrayEquals([], arr);
  assertArrayEquals([], m);
}

function testArrayModelSpliceDeleteTooMany() {
  var arr = ['a', 'b', 'c'];
  var m = arr;

  var expected = {};
  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals(expected.mutation, c.mutation);
    assertEquals(expected.index, c.index);
    assertArrayEquals(expected.removed, c.removed);
    assertArrayEquals(expected.added, c.added);
    count++;
  });

  expected.mutation = 'splice';
  expected.index = 2;
  expected.removed = ['c'];
  expected.added = [];

  m.splice(2, 3);
  Model.dirtyCheck();
  assertArrayEquals(['a', 'b'], m);
}

function testArrayModelLength() {
  var m = [0, 1];

  var expected = {};
  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals(expected.mutation, c.mutation);
    assertEquals(expected.index, c.index);
    assertArrayEquals(expected.removed, c.removed);
    assertArrayEquals(expected.added, c.added);
    count++;
  });

  expected.mutation = 'splice';
  expected.index = 2;
  expected.removed = [];
  expected.added = [ , , , ];

  m.length = 5;

  Model.dirtyCheck();
  assertArrayEquals([0, 1, , , ,], m);
  assertEquals(1, count);

  expected.mutation = 'splice';
  expected.index = 1;
  expected.removed = [1, , , ,];
  expected.added = [];

  m.length = 1;

  Model.dirtyCheck();
  assertArrayEquals([0], m);
  assertEquals(2, count);

  m.length = 1;

  Model.dirtyCheck();
  assertArrayEquals([0], m);
  assertEquals(2, count);
}

function testArrayModelPush() {
  var zero = {zero: 0};
  var one = {one: 1};
  var m = [zero, one];

  var expected = {};
  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals(expected.mutation, c.mutation);
    assertEquals(expected.index, c.index);
    assertArrayEquals(expected.removed, c.removed);
    assertArrayEquals(expected.added, c.added);
    count++;
  });

  var two = {two: 2};
  var three = {three: 3};
  expected.mutation = 'splice';
  expected.index = 2;
  expected.removed = [];
  expected.added = [two, three];

  m.push(two, three);

  Model.dirtyCheck();
  assertArrayEquals([zero, one, two, three], m);
  assertEquals(1, count);

  m.push();

  Model.dirtyCheck();
  assertEquals(1, count);
}

function testArrayModelPop() {
  var zero = {zero: 0};
  var one = {one: 1};
  var m = [zero, one];

  var expected = {};
  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals(expected.mutation, c.mutation);
    assertEquals(expected.index, c.index);
    assertArrayEquals(expected.removed, c.removed);
    assertArrayEquals(expected.added, c.added);
    count++;
  });

  expected.mutation = 'splice';
  expected.index = 1;
  expected.removed = [one];
  expected.added = [];

  assertEquals(one, m.pop());
  Model.dirtyCheck();
  assertArrayEquals([zero], m);
  assertEquals(1, count);

  expected.index = 0;
  expected.removed = [zero];

  assertEquals(zero, m.pop());
  Model.dirtyCheck();
  assertArrayEquals([], m);
  assertEquals(2, count);

  assertUndefined,(m.pop());
  Model.dirtyCheck();
  assertArrayEquals([], m);
  assertEquals(2, count);
}

function testArrayModelShift() {
  var zero = {zero: 0};
  var one = {one: 1};
  var m = [zero, one];

  var expected = {};
  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals(expected.mutation, c.mutation);
    assertEquals(expected.index, c.index);
    assertArrayEquals(expected.removed, c.removed);
    assertArrayEquals(expected.added, c.added);
    count++;
  });

  expected.mutation = 'splice';
  expected.index = 0;
  expected.removed = [zero];
  expected.added = [];

  assertEquals(zero, m.shift());
  Model.dirtyCheck();
  assertArrayEquals([one], m);
  assertEquals(1, count);

  expected.index = 0;
  expected.removed = [one];

  assertEquals(one, m.shift());
  Model.dirtyCheck();
  assertArrayEquals([], m);
  assertEquals(2, count);

  assertUndefined,(m.shift());
  Model.dirtyCheck();
  assertArrayEquals([], m);
  assertEquals(2, count);
}

function testArrayModelUnshift() {
  var zero = {zero: 0};
  var one = {one: 1};
  var m = [zero, one];

  var expected = {};
  var count = 0;
  Model.observePropertySet(m, function(c) {
    assertEquals(expected.mutation, c.mutation);
    assertEquals(expected.index, c.index);
    assertArrayEquals(expected.removed, c.removed);
    assertArrayEquals(expected.added, c.added);
    count++;
  });

  var negOne = {negOne: -1};
  expected.mutation = 'splice';
  expected.index = 0;
  expected.removed = [];
  expected.added = [negOne];

  assertEquals(3, m.unshift(negOne));
  Model.dirtyCheck();
  assertArrayEquals([negOne, zero, one], m);
  assertEquals(1, count);

  var negTwo = {negTwo: -2};
  var negThree = {negThree: -3};
  expected.added = [negThree, negTwo];

  assertEquals(5, m.unshift(negThree, negTwo));
  Model.dirtyCheck();
  assertArrayEquals([negThree, negTwo, negOne, zero, one], m);
  assertEquals(2, count);

  assertUndefined,(m.unshift());
  Model.dirtyCheck();
  assertArrayEquals([negThree, negTwo, negOne, zero, one], m);
  assertEquals(2, count);
}

function testArrayModelIndexOf() {
  var zero = {zero: 0};
  var one = {one: 1};
  var m = [zero, one];
  var zeroModel = m[0];
  var oneModel = m[1];

  assertEquals(0, m.indexOf(zero));
  assertEquals(0, m.indexOf(zeroModel));
  assertEquals(1, m.indexOf(one));
  assertEquals(1, m.indexOf(oneModel));
  assertEquals(-1, m.indexOf({foo: 'bar'}));
}

// |tracker| and |arrayTrackerObserver| are both used in the testArrayTracker*
// tests below.
var tracker;
var arrayTrackerObserver = function(mutations) {
  mutations.forEach(function(mutation) {
    tracker.addMutation(mutation);
  });
}

function assertArrayTracker() {
  tracker.notify();
  assertArrayEquals(tracker.target, tracker.copy);
}

function testArrayTrackerContained() {
  var m = ['a', 'b'];

  tracker = new ArrayTracker(m);

  m.splice(1, 1);
  m.unshift('c', 'd', 'e');
  m.splice(1, 2, 'f');

  assertArrayTracker();
}

function testArrayTrackerDeleteEmpty() {
  var m = [];

  tracker = new ArrayTracker(m);

  delete m[0];
  m.splice(0, 0, 'a', 'b', 'c');

  assertArrayTracker();
}

function testArrayTrackerRightNonOverlap() {
  var m = ['a', 'b', 'c', 'd'];

  tracker = new ArrayTracker(m);

  m.splice(0, 1, 'e');
  m.splice(2, 1, 'f', 'g');

  assertArrayTracker();
}

function testArrayTrackerLeftNonOverlap() {
  var m = ['a', 'b', 'c', 'd'];

  tracker = new ArrayTracker(m);

  m.splice(3, 1, 'f', 'g');
  m.splice(0, 1, 'e');

  assertArrayTracker();
}

function testArrayTrackerRightAdjacent() {
  var m = ['a', 'b', 'c', 'd'];

  tracker = new ArrayTracker(m);

  m.splice(1, 1, 'e');
  m.splice(2, 1, 'f', 'g');

  assertArrayTracker();
}

function testArrayTrackerLeftAdjacent() {
  var m = ['a', 'b', 'c', 'd'];

  tracker = new ArrayTracker(m);

  m.splice(2, 2, 'e');
  m.splice(1, 1, 'f', 'g');

  assertArrayTracker();
}

function testArrayTrackerRightOverlap() {
  var m = ['a', 'b', 'c', 'd'];

  tracker = new ArrayTracker(m);

  m.splice(1, 1, 'e');
  m.splice(1, 1, 'f', 'g');

  assertArrayTracker();
}

function testArrayTrackerLeftOverlap() {
  var m = ['a', 'b', 'c', 'd'];

  tracker = new ArrayTracker(m);

  m.splice(2, 1, 'e', 'f', 'g');  // a b [e f g] d
  m.splice(1, 2, 'h', 'i', 'j'); // a [h i j] f g d

  assertArrayTracker();
}

function testArrayTrackerUpdateDelete() {
  var m = ['a', 'b', 'c', 'd'];

  tracker = new ArrayTracker(m);

  m.splice(2, 1, 'e', 'f', 'g');  // a b [e f g] d
  m[0] = 'h';
  delete m[1];

  assertArrayTracker();
}

var valMax = 16;
var arrayLengthMax = 16;

function randInt(start, end) {
  return Math.round(Math.random()*(end-start) + start);
}

function randArray() {
  var args = [];
  var count = randInt(0, arrayLengthMax);

  while(count-- > 0) {
    args.push(randInt(0, valMax));
  }

  return args;
}

function randomArrayOperation(arr) {
  function empty() {
    return [];
  }

  var operations = {
    push: randArray,
    unshift: randArray,
    pop: empty,
    shift: empty,
    splice: function() {
      var args = [];
      args.push(randInt(-arr.length*2, arr.length*2), randInt(0, arr.length*2));
      args = args.concat(randArray());
      return args;
    }
  };

  // Do a splice once for each of the other operations.
  var operationList = ['splice', 'update',
                       'splice', 'delete',
                       'splice', 'push',
                       'splice', 'pop',
                       'splice', 'shift',
                       'splice', 'unshift'];

  var operation = operationList[randInt(0, operationList.length - 1)];
  if (operation == 'delete') {
    var index = randInt(0, arr.length - 1);
    delete arr[index];
  } else if (operation == 'update') {
    arr[randInt(0, arr.length - 1)] = randInt(0, valMax);
  } else {
    var opArgs = operations[operation]();
    var func = arr[operation];
    func.apply(arr, opArgs);
  }
}

function randomArrayOperations(arr, count) {
  for (var i = 0; i < count; i++) {
    randomArrayOperation(arr);
  }
}

var testCount = 32;
var operationCount = 128;

function testArrayTrackerFuzzer() {
  console.log('Fuzzing spliceProjection ' + testCount +
              ' passes with ' + operationCount + ' operations each.');
  for (var i = 0; i < testCount; i++) {
    var m = randArray();

    tracker = new ArrayTracker(m);

    randomArrayOperations(m, operationCount);

    assertArrayTracker();
  }
}

function testArrayTrackerFuzzerNoProxies() {
  ArrayTracker.forceSpliceRecalc = true;

  testArrayTrackerFuzzer();

  ArrayTracker.forceSpliceRecalc = false;
}

function assertEditDistance(tracker, distance) {
  var splices = tracker.splices || [];
  var calcDistance = 0;
  splices.forEach(function(splice) {
    calcDistance += splice.addCount;
    calcDistance += splice.deleteCount;
  });

  assertEquals(distance, calcDistance);
}

function testArrayTrackerNoProxiesEdits() {
  var data;
  var tracker;

  data = '';
  tracker = new ArrayTracker(data);
  tracker.target = '123';
  tracker.generateSplices();
  assertEditDistance(tracker, 3);

  data = 'xxxx123';
  tracker = new ArrayTracker(data);
  tracker.target = '123yyyy';
  tracker.generateSplices();
  assertEditDistance(tracker, 8);

  data = '12345';
  tracker = new ArrayTracker(data);
  tracker.target = 'a2yy45zz';
  tracker.generateSplices();
  assertEditDistance(tracker, 7);
}

function testSetValueAtPath() {
  var m = {a: 1};
  Model.setValueAtPath(m, 'a', 2);
  assertEquals(2, m.a);

  var o = {b: 3};
  Model.setValueAtPath(m, 'a', o);
  assertEquals(o, m.a);
  assertEquals(3, m.a.b);

  Model.setValueAtPath(m, 'a.b', 4);
  assertEquals(4, m.a.b);

  // Ignored because m.a.b is not an object.
  Model.setValueAtPath(m, 'a.b.c', 5);
  assertUndefined(m.a.b.c);
  assertEquals(4, m.a.b);

  Model.setValueAtPath(m, 'a.d', 6);
  assertEquals(6, m.a.d);
}

function testReentrancy() {
  var m = {};
  var count = 0;
  Model.observe(m, 'foo', function() {
    if (count++ > 1) {
      fail('Should not reenter since no changes were made');
    } else {
      Model.dirtyCheck();
    }
  });
  m.foo = 'bar';
  Model.dirtyCheck();
}

</script>
</body>
</html>
